import httpx
import pytest
import pytest_asyncio
from datasette.app import Datasette


@pytest_asyncio.fixture
async def datasette(ds_client):
    await ds_client.ds.invoke_startup()
    return ds_client.ds


@pytest_asyncio.fixture
async def datasette_with_permissions():
    """A datasette instance with permission restrictions for testing"""
    ds = Datasette(config={"databases": {"test_db": {"allow": {"id": "admin"}}}})
    await ds.invoke_startup()
    db = ds.add_memory_database("test_datasette_with_permissions", name="test_db")
    await db.execute_write(
        "create table if not exists test_table (id integer primary key, name text)"
    )
    await db.execute_write(
        "insert or ignore into test_table (id, name) values (1, 'Alice')"
    )
    # Trigger catalog refresh
    await ds.client.get("/")
    return ds


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "method,path,expected_status",
    [
        ("get", "/", 200),
        ("options", "/", 200),
        ("head", "/", 200),
        ("put", "/", 405),
        ("patch", "/", 405),
        ("delete", "/", 405),
    ],
)
async def test_client_methods(datasette, method, path, expected_status):
    client_method = getattr(datasette.client, method)
    response = await client_method(path)
    assert isinstance(response, httpx.Response)
    assert response.status_code == expected_status
    # Try that again using datasette.client.request
    response2 = await datasette.client.request(method, path)
    assert response2.status_code == expected_status


@pytest.mark.asyncio
@pytest.mark.parametrize("prefix", [None, "/prefix/"])
async def test_client_post(datasette, prefix):
    original_base_url = datasette._settings["base_url"]
    try:
        if prefix is not None:
            datasette._settings["base_url"] = prefix
        response = await datasette.client.post(
            "/-/messages",
            data={
                "message": "A message",
            },
        )
        assert isinstance(response, httpx.Response)
        assert response.status_code == 302
        assert "ds_messages" in response.cookies
    finally:
        datasette._settings["base_url"] = original_base_url


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "prefix,expected_path", [(None, "/asgi-scope"), ("/prefix/", "/prefix/asgi-scope")]
)
async def test_client_path(datasette, prefix, expected_path):
    original_base_url = datasette._settings["base_url"]
    try:
        if prefix is not None:
            datasette._settings["base_url"] = prefix
        response = await datasette.client.get("/asgi-scope")
        path = response.json()["path"]
        assert path == expected_path
    finally:
        datasette._settings["base_url"] = original_base_url


@pytest.mark.asyncio
async def test_skip_permission_checks_allows_forbidden_access(
    datasette_with_permissions,
):
    """Test that skip_permission_checks=True bypasses permission checks"""
    ds = datasette_with_permissions

    # Without skip_permission_checks, anonymous user should get 403 for protected database
    response = await ds.client.get("/test_db.json")
    assert response.status_code == 403

    # With skip_permission_checks=True, should get 200
    response = await ds.client.get("/test_db.json", skip_permission_checks=True)
    assert response.status_code == 200
    data = response.json()
    assert data["database"] == "test_db"


@pytest.mark.asyncio
async def test_skip_permission_checks_on_table(datasette_with_permissions):
    """Test skip_permission_checks works for table access"""
    ds = datasette_with_permissions

    # Without skip_permission_checks, should get 403
    response = await ds.client.get("/test_db/test_table.json")
    assert response.status_code == 403

    # With skip_permission_checks=True, should get table data
    response = await ds.client.get(
        "/test_db/test_table.json", skip_permission_checks=True
    )
    assert response.status_code == 200
    data = response.json()
    assert data["rows"] == [{"id": 1, "name": "Alice"}]


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "method", ["get", "post", "put", "patch", "delete", "options", "head"]
)
async def test_skip_permission_checks_all_methods(datasette_with_permissions, method):
    """Test that skip_permission_checks works with all HTTP methods"""
    ds = datasette_with_permissions

    # All methods should work with skip_permission_checks=True
    client_method = getattr(ds.client, method)
    response = await client_method("/test_db.json", skip_permission_checks=True)
    # We don't check status code since some methods might not be allowed,
    # but we verify the request doesn't fail due to permissions
    assert isinstance(response, httpx.Response)


@pytest.mark.asyncio
async def test_skip_permission_checks_request_method(datasette_with_permissions):
    """Test that skip_permission_checks works with client.request()"""
    ds = datasette_with_permissions

    # Without skip_permission_checks
    response = await ds.client.request("GET", "/test_db.json")
    assert response.status_code == 403

    # With skip_permission_checks=True
    response = await ds.client.request(
        "GET", "/test_db.json", skip_permission_checks=True
    )
    assert response.status_code == 200


@pytest.mark.asyncio
async def test_skip_permission_checks_isolated_to_request(datasette_with_permissions):
    """Test that skip_permission_checks doesn't affect other concurrent requests"""
    ds = datasette_with_permissions

    # First request with skip_permission_checks=True should succeed
    response1 = await ds.client.get("/test_db.json", skip_permission_checks=True)
    assert response1.status_code == 200

    # Subsequent request without it should still get 403
    response2 = await ds.client.get("/test_db.json")
    assert response2.status_code == 403

    # And another with skip should succeed again
    response3 = await ds.client.get("/test_db.json", skip_permission_checks=True)
    assert response3.status_code == 200


@pytest.mark.asyncio
async def test_skip_permission_checks_with_admin_actor(datasette_with_permissions):
    """Test that skip_permission_checks works even when actor is provided"""
    ds = datasette_with_permissions

    # Admin actor should normally have access
    admin_cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})}
    response = await ds.client.get("/test_db.json", cookies=admin_cookies)
    assert response.status_code == 200

    # Non-admin actor should get 403
    user_cookies = {"ds_actor": ds.client.actor_cookie({"id": "user"})}
    response = await ds.client.get("/test_db.json", cookies=user_cookies)
    assert response.status_code == 403

    # Non-admin actor with skip_permission_checks=True should get 200
    response = await ds.client.get(
        "/test_db.json", cookies=user_cookies, skip_permission_checks=True
    )
    assert response.status_code == 200


@pytest.mark.asyncio
async def test_skip_permission_checks_shows_denied_tables():
    """Test that skip_permission_checks=True shows tables from denied databases in /-/tables.json"""
    ds = Datasette(
        config={
            "databases": {
                "fixtures": {"allow": False}  # Deny all access to this database
            }
        }
    )
    await ds.invoke_startup()
    db = ds.add_memory_database("fixtures")
    await db.execute_write(
        "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)"
    )
    await db.execute_write("INSERT INTO test_table (id, name) VALUES (1, 'Alice')")
    await ds._refresh_schemas()

    # Without skip_permission_checks, tables from denied database should not appear in /-/tables.json
    response = await ds.client.get("/-/tables.json")
    assert response.status_code == 200
    data = response.json()
    table_names = [match["name"] for match in data["matches"]]
    # Should not see any fixtures tables since access is denied
    fixtures_tables = [name for name in table_names if name.startswith("fixtures:")]
    assert len(fixtures_tables) == 0

    # With skip_permission_checks=True, tables from denied database SHOULD appear
    response = await ds.client.get("/-/tables.json", skip_permission_checks=True)
    assert response.status_code == 200
    data = response.json()
    table_names = [match["name"] for match in data["matches"]]
    # Should see fixtures tables when permission checks are skipped
    assert "fixtures: test_table" in table_names


@pytest.mark.asyncio
async def test_in_client_returns_false_outside_request(datasette):
    """Test that datasette.in_client() returns False outside of a client request"""
    assert datasette.in_client() is False


@pytest.mark.asyncio
async def test_in_client_returns_true_inside_request():
    """Test that datasette.in_client() returns True inside a client request"""
    from datasette import hookimpl, Response

    class TestPlugin:
        __name__ = "test_in_client_plugin"

        @hookimpl
        def register_routes(self):
            async def test_view(datasette):
                # Assert in_client() returns True within the view
                assert datasette.in_client() is True
                return Response.json({"in_client": datasette.in_client()})

            return [
                (r"^/-/test-in-client$", test_view),
            ]

    ds = Datasette()
    await ds.invoke_startup()
    ds.pm.register(TestPlugin(), name="test_in_client_plugin")
    try:

        # Outside of a client request, should be False
        assert ds.in_client() is False

        # Make a request via datasette.client
        response = await ds.client.get("/-/test-in-client")
        assert response.status_code == 200
        assert response.json()["in_client"] is True

        # After the request, should be False again
        assert ds.in_client() is False
    finally:
        ds.pm.unregister(name="test_in_client_plugin")


@pytest.mark.asyncio
async def test_in_client_with_skip_permission_checks():
    """Test that in_client() works regardless of skip_permission_checks value"""
    from datasette import hookimpl
    from datasette.utils.asgi import Response

    in_client_values = []

    class TestPlugin:
        __name__ = "test_in_client_skip_plugin"

        @hookimpl
        def register_routes(self):
            async def test_view(datasette):
                in_client_values.append(datasette.in_client())
                return Response.json({"in_client": datasette.in_client()})

            return [
                (r"^/-/test-in-client$", test_view),
            ]

    ds = Datasette(config={"databases": {"test_db": {"allow": {"id": "admin"}}}})
    await ds.invoke_startup()
    ds.pm.register(TestPlugin(), name="test_in_client_skip_plugin")
    try:

        # Request without skip_permission_checks
        await ds.client.get("/-/test-in-client")
        # Request with skip_permission_checks=True
        await ds.client.get("/-/test-in-client", skip_permission_checks=True)

        # Both should have detected in_client as True
        assert (
            len(in_client_values) == 2
        ), f"Expected 2 values, got {len(in_client_values)}"
        assert all(in_client_values), f"Expected all True, got {in_client_values}"
    finally:
        ds.pm.unregister(name="test_in_client_skip_plugin")
